package com.mobdev.notepad.shared.helipad;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;

import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

import com.localytics.android.LocalyticsSession;
import com.mobdev.notepad.shared.AbstractSyncActivity;
import com.mobdev.notepad.shared.AbstractSyncThread;
import com.mobdev.notepad.shared.DBConstants;
import com.mobdev.notepad.shared.LocalyticsConstants;
import com.mobdev.notepad.shared.NotepadStrings;


/*
 * Helicoid XML Api defined at:
 *
 * http://pad.helicoid.net/document/public/6313d317
 */
public class SyncThread extends AbstractSyncThread
{

	private static final boolean DEBUG = false;

	private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz");

	  /**
     * The columns we are interested in from the database
     */
    private static final String[] PROJECTION = new String[] {
    	DBConstants._ID,
        DBConstants.REMOTE_ID,
        DBConstants.TITLE,
        DBConstants.CONTENTS,
        DBConstants.TAGS,
        DBConstants.CREATED_ON,
        DBConstants.MODIFIED_ON,
        DBConstants.SYNCED_ON,
        DBConstants.NEW_NOTE_CREATED_ON_PHONE,
        DBConstants.DELETED_FROM_PHONE,
        DBConstants.DONT_SYNC_THIS_NOTE,
    };

    protected static int IDX = 0;
    protected static final int COLUMN_INDEX_ID = IDX++;
    protected static final int COLUMN_INDEX_HELIPAD_ID = IDX++;
    protected static final int COLUMN_INDEX_TITLE = IDX++;
    protected static final int COLUMN_INDEX_CONTENTS = IDX++;
    protected static final int COLUMN_INDEX_TAGS = IDX++;
    protected static final int COLUMN_INDEX_CREATED_ON = IDX++;
    protected static final int COLUMN_INDEX_MODIFIED_ON = IDX++;
    protected static final int COLUMN_INDEX_SYNCED_ON = IDX++;
    protected static final int COLUMN_INDEX_NEW_NOTE_CREATED_ON_PHONE = IDX++;
    protected static final int COLUMN_INDEX_DELETED_FROM_PHONE = IDX++;
    protected static final int COLUMN_INDEX_DONT_SYNC_THIS_NOTE = IDX++;





	protected final AbstractSyncActivity activity;


	// Need handler for callbacks to the UI thread
    final Handler mHandler = new Handler();

	protected volatile boolean stop = false;

	protected final StringBuffer fatalError = new StringBuffer(500);
	protected final StringBuffer errorWhileSyncingTheseTitles = new StringBuffer(500);
	protected boolean someNotesCouldntBeDownloaded = false;

	protected String username = null;
	protected String password = null;

	protected Runnable threadFinishedHandler = null;

	protected SharedPreferences preferences = null;

	protected long lastSyncTime = 0; //seconds

	/**
	 * Constructor.
	 * @param activity
	 */
	public SyncThread(AbstractSyncActivity activity)
	{
		this.activity = activity;

		preferences = PreferenceManager.getDefaultSharedPreferences(activity);
		username = preferences.getString("account_username", null);
		password = preferences.getString("account_password", null);
	}


	/**
	 * Thread run method.
	 */
	@Override
	public void run()
	{

		final AbstractSyncActivity activity = this.activity;
		final String username = this.username;
		final String password = this.password;


		if(DEBUG)
			Log.i(NotepadStrings.LOG_TAG, "Sync thread started");

		//make sure username, password have been set
		if(username == null || password == null || username.trim().equals("") || password.trim().equals(""))
		{
			if(DEBUG)
				Log.i(NotepadStrings.LOG_TAG, "Username, password not set");

			setText("Please set username, password.\n\nOpening settings..", null);
			try {         Thread.sleep(3000); } catch(Exception e) {}
			activity.showPreferences();

			activity.finish();

			return;
		}

		try
        {
			lastSyncTime = preferences.getLong("last_sync_time", 0);

			/* TRY TO LOGIN TO HELIPAD */
			final int serverClockLag = loginToHelipad();


			/* CHECK THAT PHONE AND SERVER CLOCKS ARE IN SYNC */
			if(serverClockLag != Integer.MAX_VALUE && Math.abs(serverClockLag) > 60 )
			{
				if(serverClockLag > 0)
				{
					localyticsTagEvent(LocalyticsConstants.SYNC_ERROR_PHONE_CLOCK_IS_AHEAD);
					setText("WARNING:\nYour clock is ahead of\nHelipad's server clock by\n" + (serverClockLag/60) + " minutes.\n\nSync may not work correctly.", null );
				}
				else
				{
					localyticsTagEvent(LocalyticsConstants.SYNC_ERROR_PHONE_CLOCK_IS_BEHIND);
					setText("WARNING:\nYour clock is behind\nHelipad's server clock by\n" + (-serverClockLag/60) + " minutes.\n\nSync may not work correctly.", null );
				}

				int result = askUser(new String[] { "Continue", "Cancel" });
				if(result == 1)
				{
					return;
				}
			}

			/* UPLOAD ALL NEW NOTES TO THE SERVER */
			uploadNewNotesToServer();

			/* SYNC EXISTING NOTES */
			compareExistingNotes(serverClockLag);

			/* UPLOAD ANY NOTES THAT WERE MODIFIED ON THE PHONE, BUT NOT ON THE SERVER, AND SO, WEREN'T TOUCHED BY PREVIOUS METHOD */
			uploadModifiedNotesToServer(serverClockLag);

			/* DELETE NOTES THAT HAVE BEEN DELETED ON THE SERVER */
			deleteNotesDeletedOnTheServer(serverClockLag);

			Editor e = preferences.edit();
			e.putLong("last_sync_time", System.currentTimeMillis()/1000 - 2*24*60*60); //subtract 2 days just to be safe. Its ok if notes are downloaded twice.
			e.commit();

        }
		catch(IOException e)
		{
			Log.e(NotepadStrings.LOG_TAG, "IOException: " + e, e);
		}
        catch(Exception e)
        {
        	Log.e(NotepadStrings.LOG_TAG, "Unexpected exception: " + e, e);
        	if(fatalError.length() > 0)
        		fatalError.delete(0, fatalError.length() - 1);
            fatalError.append("Unexpected error occured");
        }
        finally
        {
        	if(threadFinishedHandler != null)
        	{
        		threadFinishedHandler.run();
        	}

        	if(fatalError.length() > 0) {
		    	setText("Error:  " + fatalError.toString(), null);
        	}
        	else if(errorWhileSyncingTheseTitles.length() > 0 || someNotesCouldntBeDownloaded)
        	{
				if(errorWhileSyncingTheseTitles.length() > 0)
        			setText("Error - couldn't sync:\n", errorWhileSyncingTheseTitles.toString());
        		if(someNotesCouldntBeDownloaded)
        			setText("Error: some notes failed to download", null);
        	}
        	else
        	{
        		setText("Success! All notes synced.", null);
        	}

        	try { Thread.sleep(1800); } catch(Exception e) {}
			activity.finish();
        }
	}



	/**
	 * Attempts to login to helipad. If this fails for any reason, fatalError string buffer is set, and an IOException is thrown.
	 *
	 * @return The server lag - which == (phone time - server time) in seconds, or Integer.MAX_VALUE if server clock is unknown
	 * @throws IOException If unable to login for any reason
	 */
	private int loginToHelipad() throws IOException {

		setText("Logging in..", null);

		//final SyncActivity activity = this.activity;
		final String username = this.username;
		final String password = this.password;

		String success;
		String loginResult;
		final int[] serverClockLag = new int[1];
		try
		{
			loginResult = makeAPICall("http://pad.helicoid.net/authenticate?email="+username+"&password="+password, null, serverClockLag);
			success = parseValueFromResponse(loginResult, "authenticated");
		}
		catch(IOException e)
		{
			fatalError.append("Could not connect");
			throw e;
		}
		catch(ParseException e)
		{

			//TODO LOCALYTICS TAG
			fatalError.append("Unexpected response from server");
			throw new IOException("Unexpected response from server: " + e);
		}

		if(!success.toLowerCase().equals("true")) {
			fatalError.append("Login failed.\n\nCheck username, password.");
			throw new IOException("Unexpected response from server: " + loginResult);
		}

		if(stop)
		{
			fatalError.append("canceled");
			throw new IOException("Stop requested");
		}

		return serverClockLag[0];
	}


	/**
	 * Attempts to upload all new notes created on the phone to the server.
	 * Also, deletes records of notes that have been marked for deletion on the phone, and have never been uploaded to the server.
	 *
	 * @throws IOException
	 */
	private void uploadNewNotesToServer() throws IOException
	{

		// Get URI of notes database
		//final ContentResolver contentResolver = activity.getContentResolver();
		final Uri tableUri = activity.getIntent().getData();

		// Perform a managed query. The Activity will handle closing and requerying the cursor when needed.
		// Find all notes that have been created on the phone
        Cursor newItemsQuery = activity.managedQuery(tableUri, PROJECTION,
        		"("+DBConstants.NEW_NOTE_CREATED_ON_PHONE+"=1)", // AND " + "("+DBConstants.DELETED_FROM_PHONE+"=0)",
        		null, DBConstants.DEFAULT_SORT_ORDER);

        //CursorLoader cursorLoader = new CursorLoader(activity, tableUri, PROJECTION,
        //		"("+DBConstants.NEW_NOTE_CREATED_ON_PHONE+"=1)", // AND " + "("+DBConstants.DELETED_FROM_PHONE+"=0)",
        //		null, DBConstants.DEFAULT_SORT_ORDER);
        //Cursor newItemsQuery = cursorLoader.loadInBackground();

        try
        {
	        newItemsQuery.moveToFirst();
	        while(!newItemsQuery.isAfterLast())
	        {
	        	//get note details from db
	        	final int _id = newItemsQuery.getInt( COLUMN_INDEX_ID );
	        	final boolean is_deleted_from_phone = ( newItemsQuery.getInt( COLUMN_INDEX_DELETED_FROM_PHONE ) != 0 );
	        	final Uri noteUri = ContentUris.withAppendedId(tableUri, _id);

	        	if(is_deleted_from_phone)
	        	{
	        		//this note was created on the phone and was deleted before being synced even once
	        		if(DEBUG)
		        		Log.i(NotepadStrings.LOG_TAG, "NEW NOTE ON PHONE - marked for deletion. Deleting db record ["+ _id + "] without uploading.");

	        		deleteNoteFromPhone(noteUri);
	        	}
	        	else
	        	{
	        		//this note was created on the phone and needs to be uploaded for the 1st time

	        		//upload the note to the server
		        	final String title = newItemsQuery.getString( COLUMN_INDEX_TITLE );
		        	final String contents = newItemsQuery.getString( COLUMN_INDEX_CONTENTS );
		        	final String tagsSeparatedWithSpaces = newItemsQuery.getString( COLUMN_INDEX_TAGS );


		        	boolean created = createNoteOnServer(_id, title, contents, tagsSeparatedWithSpaces);
		        	if(!created)
		        	{
		        		newItemsQuery.moveToNext();
		        		continue;
		        	}
				}

	        	if(stop)
				{
					fatalError.append("canceled");
					throw new IOException("Stop requested");
				}

	        	newItemsQuery.moveToNext();
	        }

        }
        finally
        {
	        newItemsQuery.close();
	        newItemsQuery = null;
        }
	}


	private boolean createNoteOnServer(int _id, String title, String contents, String tagsSeparatedWithSpaces) throws IOException
	{
		final ContentResolver contentResolver = activity.getContentResolver();
		final Uri tableUri = activity.getIntent().getData();
		final Uri noteUri = ContentUris.withAppendedId(tableUri, _id);

        setText("Uploading:\n", title.replace('\n', ' '));

    	if(DEBUG)
    		Log.i(NotepadStrings.LOG_TAG, "UPLOADING NEW NOTE: [" + _id + "]: " + title);

    	//upload
    	final String requestString = "" +
    	"<request>" +
    		getAuthenticationSection() +
    		getDocumentSection(title, contents, tagsSeparatedWithSpaces) +
    	"</request>";

    	String result = "";
    	try {
        	result = makeAPICall("http://pad.helicoid.net/document/create", requestString, null);
    	} catch(IOException e) {
    		fatalError.append("Couldn't upload note: " + title.replace('\n', ' ') + "\n");
    		throw e;
    	}

    	//parse response
    	String saved;
    	String helipadId;
    	try  {
    		saved = parseValueFromResponse(result, "saved");
    		helipadId = parseValueFromResponse(result, "id");
    	} catch(ParseException e) {
    		if(DEBUG)
    			Log.e(NotepadStrings.LOG_TAG, "Couldn't parse response when uploading new note:\n" + result + "\nRequest was: \n" + requestString, e);
    		else
    			Log.e(NotepadStrings.LOG_TAG, "Couldn't parse response when uploading new note:\n" + result, e);

    		//TODO LOCALYTICS TAG
    		errorWhileSyncingTheseTitles.append(title.replace('\n', ' ') + "\n");
    		return false;
    	}

    	if(!saved.equals("true"))
    	{
    		if(DEBUG)
    			Log.e(NotepadStrings.LOG_TAG, "Response <saved> != true:\n" + result + "\nRequest was: \n" + requestString );
    		else
    			Log.e(NotepadStrings.LOG_TAG, "Response <saved> != true:\n" + result );

    		errorWhileSyncingTheseTitles.append(title.replace('\n', ' ') + "\n");
    		return false;
    	}

    	//update table - mark note as not new, update helipad id, sync time
    	int now = (int) (System.currentTimeMillis() / 1000);

        ContentValues values = new ContentValues();
        values.put(DBConstants.NEW_NOTE_CREATED_ON_PHONE, 0);
        values.put(DBConstants.REMOTE_ID, helipadId);
        values.put(DBConstants.SYNCED_ON, now);

        int updated = contentResolver.update(noteUri, values, null, null);
    	if(updated <= 0) {
			//TODO LOCALYTICS TAG
    		fatalError.append("Couldn't update database" + "\n");
    		throw new IOException("Couldn't update note: " + noteUri);
    	}


    	return true;
	}


	/**
	 * Sync existing notes.
	 * @param serverClockLag The difference (phone clock - server clock) in seconds, as returned by loginToHelipad()
	 * @return
	 * @throws IOException
	 */
	private void compareExistingNotes(int serverClockLag) throws IOException
	{
		final ContentResolver contentResolver = activity.getContentResolver();
		final Uri tableUri = activity.getIntent().getData();

		//boolean conflictTagged = false; //tag only one conflict per sync

		/* DOWNLOAD NOTES FROM THE SERVER */
		setText("Getting data..  ", null);


		//download
		String documentsResponse = "";
		try
		{
			final String requestString = "" +
			"<request>" +
				getAuthenticationSection() +
				"<changed_since>" + lastSyncTime + "</changed_since>" +
			"</request>";

			documentsResponse = makeAPICall("http://pad.helicoid.net/documents", requestString, null);
		} catch(IOException e) {
			fatalError.append("Couldn't download notes from server\n");
			throw e;
		}


		if(stop)
		{
			fatalError.append("canceled");
			throw new IOException("Stop requested");
		}

		//make sure the api call returned a valid response
		if(!documentsResponse.contains("<documents") || !documentsResponse.contains("</documents>") )
		{
			fatalError.append("Couldn't get notes from server.\nBad internet connection?\n");
    		throw new IOException("documents api call failed");
		}

		//go through each note
		if(documentsResponse.contains("<document>"))
		{
			//if(DEBUG)
			//	Log.i(NotePad.LOG_TAG, "Documents: " + documentsResponse);

			String[] documents = documentsResponse.split("</document>");
			for( int i = 0; i < documents.length - 1; i++  )
			{
				String document = documents[i];

				/*
					<document>
						<approved type="boolean">false</approved>
						<dangerous type="boolean">false</dangerous>
						<share nil="true"></share>
						<id type="integer">18</id>
						<title>Another note created on phone...</title>
						<source>Another note created on phone1</source>
						<tags type="array"/>
						<created-on type="datetime">2009-05-25T02:57:15+01:00</created-on>
						<updated-on type="datetime">2009-05-25T02:57:15+01:00</updated-on>
					</document>
				 */

		    	//parse values from server response
				ServerSideNoteInfo serverSideNoteInfo = new ServerSideNoteInfo();

				try
				{
					serverSideNoteInfo.server_helipad_id = parseValueFromResponse(document, "id");
					serverSideNoteInfo.server_title = decodeXMLEscapeChars( parseValueFromResponse(document, "title") );

					if(DEBUG)
						Log.i(NotepadStrings.LOG_TAG, "Got note: (" + serverSideNoteInfo.server_helipad_id + ") - server title:" + serverSideNoteInfo.server_title);

					serverSideNoteInfo.server_contents = decodeXMLEscapeChars( parseValueFromResponse(document, "source") );
					serverSideNoteInfo.server_tags = decodeXMLEscapeChars( parseValueFromResponse(document, "tags") );

					final String serverCreatedOnString = parseValueFromResponse(document, "created-on");
					serverSideNoteInfo.server_created_on = parseTimestamp(serverCreatedOnString);

					final String serverModifiedOnString = parseValueFromResponse(document, "updated-on");
					serverSideNoteInfo.server_modified_on = parseTimestamp(serverModifiedOnString);
				}
				catch(ParseException e)
				{
					someNotesCouldntBeDownloaded |= true;
		    		Log.e(NotepadStrings.LOG_TAG, "Couldn't parse id or update-on from document: " + document + "\n", e);
		    		continue;
				}

				//Query notes db for this document
				Cursor noteQuery = activity.managedQuery(tableUri, PROJECTION, DBConstants.REMOTE_ID+"=?", new String[] { serverSideNoteInfo.server_helipad_id }, DBConstants.DEFAULT_SORT_ORDER);
		        //CursorLoader cursorLoader = new CursorLoader(activity, tableUri, PROJECTION, DBConstants.HELIPAD_ID+"=?", new String[] { serverSideNoteInfo.server_helipad_id }, DBConstants.DEFAULT_SORT_ORDER);
		        //Cursor noteQuery = cursorLoader.loadInBackground();

				try
				{
			        if(noteQuery.getCount() == 0)
			        {
			        	//NOTE WAS CREATED ON THE SERVER

			        	if(DEBUG)
			        		Log.i(NotepadStrings.LOG_TAG, "DOWNLOADING NEW NOTE: (" + serverSideNoteInfo.server_helipad_id + "): - mod_time:" + serverSideNoteInfo.server_modified_on + ", " + new Date((long)serverSideNoteInfo.server_modified_on * 1000) + ", server title:" + serverSideNoteInfo.server_title);


			        	//insert note into db
			        	int now = (int) (System.currentTimeMillis() / 1000);
			        	ContentValues values = new ContentValues();
			            values.put(DBConstants.REMOTE_ID, serverSideNoteInfo.server_helipad_id);
			            values.put(DBConstants.TITLE, serverSideNoteInfo.server_title);
			            values.put(DBConstants.CONTENTS, serverSideNoteInfo.server_contents);
			            values.put(DBConstants.TAGS, serverSideNoteInfo.server_tags);
			            values.put(DBConstants.CREATED_ON, serverSideNoteInfo.server_created_on);
			            values.put(DBConstants.MODIFIED_ON, serverSideNoteInfo.server_modified_on );
			            values.put(DBConstants.SYNCED_ON, Math.max(now, serverSideNoteInfo.server_modified_on) ); //handle server clock being ahead of phone clock
			            values.put(DBConstants.NEW_NOTE_CREATED_ON_PHONE, 0);
			            values.put(DBConstants.DELETED_FROM_PHONE, 0);
			            values.put(DBConstants.DONT_SYNC_THIS_NOTE, 0);

			            final Uri noteUri = contentResolver.insert(tableUri, values);
						if (noteUri == null)
						{
							//TODO LOCALYTICS TAG
							//handle couldn't insert note error
							fatalError.append("Couldn't update database" + "\n");
							throw new IOException("Couldn't insert note into db: " + tableUri);
						}
			        }
			        else
			        {
			        	//NOTE HAS BEEN SYNCED WITH PHONE AT LEAST ONCE BEFORE

			        	noteQuery.moveToFirst();

			        	LocalNoteInfo localNoteInfo = new LocalNoteInfo();
			        	localNoteInfo._id = noteQuery.getInt( COLUMN_INDEX_ID );
	        			localNoteInfo.local_title = noteQuery.getString( COLUMN_INDEX_TITLE );
	        			localNoteInfo.local_contents = noteQuery.getString( COLUMN_INDEX_CONTENTS );
	        			localNoteInfo.local_tags = noteQuery.getString( COLUMN_INDEX_TAGS );
			        	localNoteInfo.is_deleted_from_phone = ( noteQuery.getInt( COLUMN_INDEX_DELETED_FROM_PHONE ) != 0 );
			        	localNoteInfo.dont_sync_this_note = noteQuery.getInt( COLUMN_INDEX_DONT_SYNC_THIS_NOTE );
			        	localNoteInfo.local_modified_on = noteQuery.getInt( COLUMN_INDEX_MODIFIED_ON );
			        	localNoteInfo.synced_on = noteQuery.getInt( COLUMN_INDEX_SYNCED_ON );


			        	if(DEBUG) {
			        		//Log.i(NotePadStrings.LOG_TAG, "Note has been synced before: (" + serverSideNoteInfo.server_helipad_id + ") title: " + serverSideNoteInfo.server_title);
			        		Log.i(NotepadStrings.LOG_TAG, "About to sync: (" + serverSideNoteInfo.server_helipad_id + "), " +
			        			"\nlocal:  " + localNoteInfo.local_modified_on + " - " + new Date((long) localNoteInfo.local_modified_on * 1000) +  ", " +
			        			"\nsynced: " + localNoteInfo.synced_on + " - " + new Date((long) localNoteInfo.synced_on * 1000) +
			        			"\nserver: " + serverSideNoteInfo.server_modified_on + " - " + new Date((long)serverSideNoteInfo.server_modified_on * 1000) + ", ");

			        	}

			        	//skip notes that have been marked as "dont_sync_this_note" when the user clicked the 'skip' button in the sync conflict resolution prompt, and the note hasn't changed since then
			    		if((localNoteInfo.dont_sync_this_note > 0) && (localNoteInfo.local_modified_on <= localNoteInfo.dont_sync_this_note) && (serverSideNoteInfo.server_modified_on <= localNoteInfo.dont_sync_this_note))
			    		{
			    			continue;
			    		}


			        	if( (localNoteInfo.local_modified_on <= localNoteInfo.synced_on) && (serverSideNoteInfo.server_modified_on <= localNoteInfo.synced_on) )
			        	{
			        		//NOTE IS CURRENTLY IN SYNC

			        		if(localNoteInfo.is_deleted_from_phone)
			        		{
				        		if(DEBUG)
			    	        		Log.i(NotepadStrings.LOG_TAG, "NOTE MARKED FOR DELETION ON PHONE: (" + serverSideNoteInfo.server_helipad_id + ") " + serverSideNoteInfo.server_title);


				        		//TODO give user way to disable this prompt in settings
			        			//note is reported to be in sync with the server, but is deleted on phone and exists on the server
				        		final String text = "The note:\n" +
			    				"\"" + serverSideNoteInfo.server_title + "\"\n" +
			    				"was deleted from phone.\n" +
			    				"Sync options:\n";
			        			handleConflict(text, localNoteInfo, serverSideNoteInfo, serverClockLag,
			        					ConflictResolutionOptions.DELETE_FROM_SERVER,
			        					ConflictResolutionOptions.DOWNLOAD_TO_PHONE,
				        				ConflictResolutionOptions.SKIP);
				        	}
			        		else
			        		{
			        			//nothing needs to be done
			        			if(DEBUG)
				        			Log.i(NotepadStrings.LOG_TAG, "NOTE ALREADY IN SYNC: (" + serverSideNoteInfo.server_helipad_id + ") " + serverSideNoteInfo.server_title);

			        		}
			        	}
			        	else if( (localNoteInfo.local_modified_on > localNoteInfo.synced_on) && (serverSideNoteInfo.server_modified_on <= localNoteInfo.synced_on) )
			        	{
			        		//NOTE NEEDS TO BE UPLOADED
			        		if(localNoteInfo.is_deleted_from_phone)
			        		{
			        			//prompt user to confirm delete from server - this isn't really a conflict, its just to make sure the user knows about and authorizes the deletion
			        			final String text = "The note:\n" +
			    				"\"" + serverSideNoteInfo.server_title + "\"\n" +
			    				"was deleted from phone.\n" +
			    				"Sync options:\n";
			        			handleConflict(text, localNoteInfo, serverSideNoteInfo, serverClockLag,
			        					ConflictResolutionOptions.DELETE_FROM_SERVER,
			        					ConflictResolutionOptions.DOWNLOAD_TO_PHONE,
				        				ConflictResolutionOptions.SKIP);
			        			/*
				        		boolean deleted = deleteNoteFromServer(localNoteInfo, serverSideNoteInfo);
				        		if(!deleted)
				        		{
				        			continue;
				        		}
				        		*/
				        	}
			        		else
			        		{
				        		boolean uploaded = uploadNoteToServer(localNoteInfo, serverSideNoteInfo, serverClockLag);
				        		if(!uploaded)
				        		{
				        			continue;
				        		}
			        		}
			        	}
			        	else if( (localNoteInfo.local_modified_on <= localNoteInfo.synced_on) && (serverSideNoteInfo.server_modified_on > localNoteInfo.synced_on) )
			        	{
			        		//NOTE NEEDS TO BE DOWNLOADED
			        		if(DEBUG)
			        			Log.i(NotepadStrings.LOG_TAG, "DOWNLOADING NOTE (" + serverSideNoteInfo.server_helipad_id + ") : " + serverSideNoteInfo.server_title);

			        		if(localNoteInfo.is_deleted_from_phone)
			        		{
			        			//note is marked as deleted on phone, but has been synced, and then modified on the server.
			        			final String text = "The note:\n" +
			    				"\"" + serverSideNoteInfo.server_title + "\"\n" +
			    				"was deleted from phone.\n" +
			    				"Sync options:\n";
			        			handleConflict(text, localNoteInfo, serverSideNoteInfo, serverClockLag,
			        					ConflictResolutionOptions.DELETE_FROM_SERVER,
			        					ConflictResolutionOptions.DOWNLOAD_TO_PHONE,
				        				ConflictResolutionOptions.SKIP);
			        		}
			        		else
			        		{
								downloadNoteToPhone(localNoteInfo, serverSideNoteInfo);
			        		}
			        	}
			        	else if( (localNoteInfo.local_modified_on > localNoteInfo.synced_on) && (serverSideNoteInfo.server_modified_on > localNoteInfo.synced_on) )
			        	{
			        		//CONFLICT
			        		if(DEBUG)
			        			Log.i(NotepadStrings.LOG_TAG, "CONFLICT ON (" + serverSideNoteInfo.server_helipad_id + ") : " + serverSideNoteInfo.server_title);

			        		if(localNoteInfo.is_deleted_from_phone)
			        		{
			        			//note was marked as deleted on the phone and modified on the server after being synced
			        			final String text = "CONFLICT:\n" +
			    				"\"" + serverSideNoteInfo.server_title + "\"\n" +
			    				"edited on server,\n" +
			    				"deleted from phone.\n" +
			    				"\n" +
			    				"To resolve:";
			        			handleConflict(text, localNoteInfo, serverSideNoteInfo, serverClockLag,
				        				ConflictResolutionOptions.DOWNLOAD_TO_PHONE,
				        				ConflictResolutionOptions.DELETE_FROM_SERVER,
				        				ConflictResolutionOptions.SKIP);
			        		}
			        		else
			        		{
			        			//note was modified on the phone and on the server after being synced
			        			final String text = "CONFLICT:\n" +
			    				"\"" + serverSideNoteInfo.server_title + "\"\n" +
			    				"edited on server\n" +
			    				"and on phone.\n" +
			    				"\n" +
			    				"To resolve:";
			        			handleConflict(text, localNoteInfo, serverSideNoteInfo, serverClockLag,
				        				ConflictResolutionOptions.DOWNLOAD_TO_PHONE,
				        				ConflictResolutionOptions.UPLOAD_TO_SERVER,
				        				ConflictResolutionOptions.SKIP);
			        		}

			        		//conflictOnTheseTitles.append(serverSideNoteInfo.server_title);
			        	}
			        	else
			        	{
			        		throw new IllegalStateException("Logic failed");
			        	}
			        } // end-if  note has been synced at least once before
				}
				finally
				{
					noteQuery.close();
					noteQuery = null;
				}
			} // for-loop over documents from server
		} //end-if  no documents found on server
		else
		{
			Log.i(NotepadStrings.LOG_TAG, "No documents found on server");
		}
	}

	/**
	 * Uploads any notes to the server that weren't modified on the server
	 * since the last_sync_time, and so
	 * @param serverClockLag
	 */
	private void uploadModifiedNotesToServer(int serverClockLag) throws IOException
	{
		final Uri tableUri = activity.getIntent().getData();

		//Query notes db for all notes that were modified after they were synced
		Cursor modifiedNotesQuery = activity.managedQuery(tableUri, PROJECTION, DBConstants.MODIFIED_ON+">"+DBConstants.SYNCED_ON, null, DBConstants.DEFAULT_SORT_ORDER);
		//CursorLoader cursorLoader = new CursorLoader(activity, tableUri, PROJECTION, DBConstants.MODIFIED_ON+">"+DBConstants.SYNCED_ON, null, DBConstants.DEFAULT_SORT_ORDER);
        //Cursor modifiedNotesQuery = cursorLoader.loadInBackground();
		modifiedNotesQuery.moveToFirst();
		try
		{

	    	while(!modifiedNotesQuery.isAfterLast())
	        {
	    		if(stop)
				{
					fatalError.append("canceled");
					throw new IOException("Stop requested");
				}


	    		//check whether this note exists on the server
        		String helipadId = modifiedNotesQuery.getString( COLUMN_INDEX_HELIPAD_ID );

        		//get note details from db
        		LocalNoteInfo localNoteInfo = new LocalNoteInfo();
	        	localNoteInfo._id = modifiedNotesQuery.getInt( COLUMN_INDEX_ID );
	        	localNoteInfo.local_title = modifiedNotesQuery.getString( COLUMN_INDEX_TITLE );
	        	localNoteInfo.local_contents = modifiedNotesQuery.getString( COLUMN_INDEX_CONTENTS );
    			localNoteInfo.local_tags = modifiedNotesQuery.getString( COLUMN_INDEX_TAGS );
	        	localNoteInfo.is_deleted_from_phone = ( modifiedNotesQuery.getInt( COLUMN_INDEX_DELETED_FROM_PHONE ) != 0 );
	        	localNoteInfo.dont_sync_this_note = modifiedNotesQuery.getInt( COLUMN_INDEX_DONT_SYNC_THIS_NOTE );
	        	localNoteInfo.local_created_on = modifiedNotesQuery.getInt( COLUMN_INDEX_CREATED_ON );
	        	localNoteInfo.local_modified_on = modifiedNotesQuery.getInt( COLUMN_INDEX_MODIFIED_ON );
	        	localNoteInfo.synced_on = modifiedNotesQuery.getInt( COLUMN_INDEX_SYNCED_ON );

	        	//skip notes that have been marked as "dont_sync_this_note" when the user clicked the 'skip' button in the sync conflict resolution prompt, and the note hasn't changed since then
	    		if((localNoteInfo.dont_sync_this_note > 0) && (localNoteInfo.local_modified_on <= localNoteInfo.dont_sync_this_note) )
	    		{
	    			modifiedNotesQuery.moveToNext();
	    			continue;
	    		}

        		//NOTE NEEDS TO BE UPLOADED
	        	ServerSideNoteInfo serverSideNoteInfo = new ServerSideNoteInfo(); //
    			serverSideNoteInfo.server_helipad_id = helipadId;
    			serverSideNoteInfo.server_title = localNoteInfo.local_title;
				serverSideNoteInfo.server_contents = localNoteInfo.local_contents;
				serverSideNoteInfo.server_tags = localNoteInfo.local_tags;
				serverSideNoteInfo.server_created_on = localNoteInfo.local_created_on;
				serverSideNoteInfo.server_modified_on = localNoteInfo.local_modified_on;

        		if(localNoteInfo.is_deleted_from_phone)
        		{
        			//prompt user to confirm delete from server - this isn't really a conflict, its just to make sure the user knows about and authorizes the deletion
        			final String text = "The note:\n" +
    				"\"" + serverSideNoteInfo.server_title + "\"\n" +
    				"was deleted from phone.\n" +
    				"Sync options:\n";
        			handleConflict(text, localNoteInfo, serverSideNoteInfo, serverClockLag,
        					ConflictResolutionOptions.DELETE_FROM_SERVER,
        					ConflictResolutionOptions.DOWNLOAD_TO_PHONE,
	        				ConflictResolutionOptions.SKIP);
        			/*
	        		boolean deleted = deleteNoteFromServer(localNoteInfo, serverSideNoteInfo);
	        		if(!deleted)
	        		{
	        			continue;
	        		}
	        		*/
	        	}
        		else
        		{
        			//Log.w("benw", "uploading ot server - local modified: " + localNoteInfo.local_modified_on + ", synced on: " + localNoteInfo.synced_on);
	        		boolean uploaded = uploadNoteToServer(localNoteInfo, serverSideNoteInfo, serverClockLag);
	        		if(!uploaded)
	        		{
	        			modifiedNotesQuery.moveToNext();
	        			continue;
	        		}
        		}

        		modifiedNotesQuery.moveToNext();

			} //while
		}
		finally
		{
			modifiedNotesQuery.close();
			modifiedNotesQuery = null;
		}
	}


	/**
	 * Deletes notes that have been deleted from the server, but haven't yet been deleted from the client.
	 * @param _idsOfNotesThatExistOnServer
	 * @param serverClockLag
	 * @throws IOException
	 */
	private void deleteNotesDeletedOnTheServer(int serverClockLag) throws IOException
	{

		if(stop)
		{
			fatalError.append("canceled");
			throw new IOException("Stop requested");
		}

		// Get URI of notes database
		//final ContentResolver contentResolver = activity.getContentResolver();
		final Uri tableUri = activity.getIntent().getData();

		//Get ids of all notes that exist on the server
		HashSet<String> helipadIdsOfNotesThatExistOnServer = new HashSet<String>();


		//download ids of all notes on the server
		String allIdsResponse = "";
		try
		{
			final String requestString = "" +
	    	"<request>" +
	    		getAuthenticationSection() +
	    	"</request>";

			allIdsResponse = makeAPICall("http://pad.helicoid.net/document/all_ids", requestString, null);
			//Log.w("BEN", "ALL IDS in DELETE CALL: " + allIdsResponse );

		} catch(IOException e) {
			fatalError.append("Couldn't download notes from server\n");
			throw e;
		}


		//make sure the api call returned a valid response
		if(!allIdsResponse.contains("<documents") || !allIdsResponse.contains("</documents>") )
		{
			fatalError.append("Couldn't get note ids from server.\nBad internet connection?\n");
    		throw new IOException("documents api call failed");
		}

		//parse all ids from response
		if(allIdsResponse.contains("<id>"))
		{
			allIdsResponse = allIdsResponse.replace("<documents>", "").replace("</documents>", "");
			for( String idString : allIdsResponse.split("</id>")  )
			{
				/*	<id>32</id>  */
				idString = idString.replace("<id>", "").trim();
				if(idString.length() != 0) {
					helipadIdsOfNotesThatExistOnServer.add(idString);
				}
			}
		}

		// Perform a managed query. The Activity will handle closing and requerying the cursor when needed.
		// Find all notes that have been created on the phone
        Cursor allNotesQuery = activity.managedQuery(tableUri, PROJECTION, null, null, DBConstants.DEFAULT_SORT_ORDER);
        //CursorLoader cursorLoader = new CursorLoader(activity, tableUri, PROJECTION, null, null, DBConstants.DEFAULT_SORT_ORDER);
        //Cursor allNotesQuery = cursorLoader.loadInBackground();

        try
        {
        	allNotesQuery.moveToFirst();
        	while(!allNotesQuery.isAfterLast())
	        {
        		//check whether this note exists on the server
        		String helipadId = allNotesQuery.getString( COLUMN_INDEX_HELIPAD_ID );
        		if( helipadIdsOfNotesThatExistOnServer.contains(helipadId) ) //TODO
        		{
        			allNotesQuery.moveToNext();
        			continue; //this note still exists on the server
        		}

        		//get note details from db
        		LocalNoteInfo localNoteInfo = new LocalNoteInfo();
	        	localNoteInfo._id = allNotesQuery.getInt( COLUMN_INDEX_ID );
	        	localNoteInfo.local_title = allNotesQuery.getString( COLUMN_INDEX_TITLE );
	        	localNoteInfo.local_contents = allNotesQuery.getString( COLUMN_INDEX_CONTENTS );
    			localNoteInfo.local_tags = allNotesQuery.getString( COLUMN_INDEX_TAGS );
	        	localNoteInfo.is_deleted_from_phone = ( allNotesQuery.getInt( COLUMN_INDEX_DELETED_FROM_PHONE ) != 0 );
	        	localNoteInfo.dont_sync_this_note = allNotesQuery.getInt( COLUMN_INDEX_DONT_SYNC_THIS_NOTE );
	        	localNoteInfo.local_modified_on = allNotesQuery.getInt( COLUMN_INDEX_MODIFIED_ON );
	        	localNoteInfo.synced_on = allNotesQuery.getInt( COLUMN_INDEX_SYNCED_ON );

	        	final Uri noteUri = ContentUris.withAppendedId(tableUri, localNoteInfo._id);

	        	//skip notes that have been marked as "dont_sync_this_note" when the user clicked the 'skip' button in the sync conflict resolution prompt, and the note hasn't changed since then
	    		if((localNoteInfo.dont_sync_this_note > 0) && (localNoteInfo.local_modified_on <= localNoteInfo.dont_sync_this_note))
	    		{
	    			//clean up any notes marked for deletetion on the phone that have been deleted on the server
	    			if(localNoteInfo.is_deleted_from_phone)
		        	{
		        		//this note was deleted from the server and from the phone
		        		if(DEBUG)
			        		Log.i(NotepadStrings.LOG_TAG, "NEW NOTE ON PHONE - marked for deletion. Deleting db record ["+ localNoteInfo._id + "] without uploading.");

		        		//completely remove the record
		        		deleteNoteFromPhone(noteUri);

		        	}

	    			allNotesQuery.moveToNext();

	    			continue;
	    		}



	        	if(localNoteInfo.local_modified_on <= localNoteInfo.synced_on)
	        	{
	        		/* DELETE THE NOTE FROM THE PHONE */
	        		//this note has been deleted on the server. Delete note record from db.

	        		//TODO give user way to disable this prompt in settings
	        		//prompt user to confirm delete from server - this isn't really a conflict, its just to make sure the user knows about and authorizes the deletion
        			final String text = "The note:\n" +
    				"\"" + localNoteInfo.local_title + "\"\n" +
    				"was deleted on server.\n" +
    				"Sync options:\n";
        			handleConflict(text, localNoteInfo, null, serverClockLag,
	        				ConflictResolutionOptions.DELETE_FROM_PHONE,
	        				ConflictResolutionOptions.CREATE_ON_SERVER,
	        				ConflictResolutionOptions.SKIP );

	        		//deleteNoteFromPhone(noteUri);
	        	}
	        	else if(localNoteInfo.local_modified_on > localNoteInfo.synced_on)
	        	{

		        	if(localNoteInfo.is_deleted_from_phone)
		        	{
		        		//this note was deleted from the server and from the phone at the same time
		        		if(DEBUG)
			        		Log.i(NotepadStrings.LOG_TAG, "NEW NOTE ON PHONE - marked for deletion. Deleting db record ["+ localNoteInfo._id + "] without uploading.");

		        		deleteNoteFromPhone(noteUri);

		        	}
		        	else
		        	{
		        		//this note was edited on the phone and deleted from the server before being synced
		        		//handle conflict
		        		final String text = "CONFLICT:\n" +
	    				"\"" + localNoteInfo.local_title + "\"\n" +
	    				"deleted on server,\n" +
	    				"edited on phone.\n" +
	    				"\n" +
	    				"To resolve:";
		        		handleConflict(text, localNoteInfo, null, serverClockLag,
		        				ConflictResolutionOptions.DELETE_FROM_PHONE,
		        				ConflictResolutionOptions.CREATE_ON_SERVER,
		        				ConflictResolutionOptions.SKIP);
		        	}
	        	}

	        	if(stop)
				{
					fatalError.append("canceled");
					throw new IOException("Stop requested");
				}

	        	allNotesQuery.moveToNext();
	        }
        }
        finally
        {
	        allNotesQuery.close();
	        allNotesQuery = null;
        }
	}


	/**
	 * Resolves conflict
	 * @param text The text to show the user
	 * @param localNoteInfo
	 * @param serverSideNoteInfo  Can be null
	 * @param serverClockLag
	 * @param option0
	 * @param option1
	 * @param option2 Can be null
	 * @throws IOException
	 */
	private void handleConflict(String text, LocalNoteInfo localNoteInfo, ServerSideNoteInfo serverSideNoteInfo, int serverClockLag,
			ConflictResolutionOptions option0,
			ConflictResolutionOptions option1,
			ConflictResolutionOptions option2) throws IOException
	{


		setText(text, null);
		int result;
		if(option2 == null)
		{
			result = askUser(new String[] { option0.toString(), option1.toString(), null });
		}
		else
		{
			result = askUser(new String[] { option0.toString(), option1.toString(), option2.toString() });
		}
		//setText("", null);

		ConflictResolutionOptions choice;
		switch(result)
		{
		case 0: choice = option0; break;
		case 1: choice = option1; break;
		case 2: choice = option2; break;
		default: throw new IllegalStateException("Unexpected choice: " + result);
		}

		final Uri tableUri = activity.getIntent().getData();
		final Uri noteUri = ContentUris.withAppendedId(tableUri, localNoteInfo._id);

		switch(choice) {
		case DELETE_FROM_PHONE:
			deleteNoteFromPhone(noteUri);
			break;
		case DELETE_FROM_SERVER:
			deleteNoteFromServer(localNoteInfo, serverSideNoteInfo);
			break;
		case DOWNLOAD_TO_PHONE:
			downloadNoteToPhone(localNoteInfo, serverSideNoteInfo);
			break;
		case UPLOAD_TO_SERVER:
			uploadNoteToServer(localNoteInfo, serverSideNoteInfo, serverClockLag);
			break;
		case CREATE_ON_SERVER:
			createNoteOnServer(localNoteInfo._id, localNoteInfo.local_title, localNoteInfo.local_contents, localNoteInfo.local_tags);
			break;
		case SKIP:

			/* If the note was deleted from either the server or the phone, don't attempt to sync the note in the future.
			 * Otherwise, if the note still exists on both, continue prompting the user to resolve the conflict on subsequent syncs.
			 */
			//if(option0 == ConflictResolutionOptions.DELETE_FROM_PHONE || option0 == ConflictResolutionOptions.DELETE_FROM_SERVER ||
			//   option1 == ConflictResolutionOptions.DELETE_FROM_PHONE || option1 == ConflictResolutionOptions.DELETE_FROM_SERVER)
			//{
			markAsDontSyncThisNote(noteUri, serverSideNoteInfo); //always mark as skip - since skip is now handled more normally
			break;
		default:
			throw new IllegalStateException("Unexpected enum value: " + choice);
		}
	}


	/**
	 * @param noteUri
	 * @param serverSideNoteInfo
	 * @throws IOException
	 */
	private void markAsDontSyncThisNote(final Uri noteUri, ServerSideNoteInfo serverSideNoteInfo) throws IOException {
		//mark the note for skipping the sync.
		final ContentResolver contentResolver = activity.getContentResolver();

		int now = (int) (System.currentTimeMillis() / 1000);
		ContentValues values = new ContentValues();
		if(serverSideNoteInfo == null)
		{
			values.put(DBConstants.DONT_SYNC_THIS_NOTE, now);
		}
		else
		{
			values.put(DBConstants.DONT_SYNC_THIS_NOTE, Math.max(now, serverSideNoteInfo.server_modified_on));
		}
		/* DON'T update 'SYNCED_ON'. This way, the conflict state is maintained, so that, if the note is edited (lets say on the phone),
		 * the note will remain in a "conflict" state, and not switch into a normal "updated" state at which point it would be synced
		 * without a prompt, and the conflicting changes would be lost.
		 *
		if(serverSideNoteInfo == null)
		{
			values.put(DBConstants.SYNCED_ON, now);
		}
		else
		{
			values.put(DBConstants.SYNCED_ON, Math.max(now, serverSideNoteInfo.server_modified_on));
		}
		*/
		int updated = contentResolver.update(noteUri, values, null, null);
		if(updated <= 0) {
			//TODO LOCALYTICS TAG
			fatalError.append("Couldn't update database" + "\n");
			throw new IOException("Couldn't update note: " + noteUri);
		}
	}






	/**
	 * @param localNoteInfo TODO
	 * @throws IOException
	 */
	private void deleteNoteFromPhone(Uri noteUri) throws IOException {

		final ContentResolver contentResolver = activity.getContentResolver();


		int updated = contentResolver.delete(noteUri, null, null);
		if(updated <= 0) {
			//TODO LOCALYTICS TAG
			fatalError.append("Couldn't update database" + "\n");
			throw new IOException("Couldn't update note: " + noteUri);
		}
	}






	/**
	 * Deletes the given note from the server
	 * @param localNoteInfo
	 * @param serverSideNoteInfo
	 * @return True if the delete was successful
	 * @throws IOException
	 */
	private boolean deleteNoteFromServer(LocalNoteInfo localNoteInfo, ServerSideNoteInfo serverSideNoteInfo) throws IOException
	{
		//final ContentResolver contentResolver = activity.getContentResolver();
		final Uri tableUri = activity.getIntent().getData();
		final Uri noteUri = ContentUris.withAppendedId(tableUri, localNoteInfo._id);

		if(DEBUG)
    		Log.i(NotepadStrings.LOG_TAG, "NOTE MARKED FOR DELETION ON PHONE. DELETING FROM SERVER: (" + serverSideNoteInfo.server_helipad_id + ") " + serverSideNoteInfo.server_title);

		//delete
		final String requestString = "" +
    	"<request>" +
    		getAuthenticationSection() +
    	"</request>";

    	setText("Deleting from server:\n", serverSideNoteInfo.server_title.replace('\n', ' '));

    	String result = "";
    	try {
        	result = makeAPICall("http://pad.helicoid.net/document/"+serverSideNoteInfo.server_helipad_id+"/destroy", requestString, null);
    	} catch(IOException e) {
    		Log.e(NotepadStrings.LOG_TAG, "Couldn't delete note from server: " + serverSideNoteInfo.server_title, e);

    		errorWhileSyncingTheseTitles.append(serverSideNoteInfo.server_title.replace('\n', ' ') + "\n");

    		return false;
    	}

    	//parse response
    	String deleted;
    	try  {
    		deleted = parseValueFromResponse(result, "deleted");
    	} catch(ParseException e) {
    		if(DEBUG)
    			Log.e(NotepadStrings.LOG_TAG, "Couldn't parse response:\n" + result + "\nRequest was: \n" + requestString, e);
    		else
    			Log.e(NotepadStrings.LOG_TAG, "Couldn't parse response:\n" + result, e);

			//TODO LOCALYTICS TAG
    		errorWhileSyncingTheseTitles.append(serverSideNoteInfo.server_title.replace('\n', ' ') + "\n");

    		return false;
    	}

    	if(!deleted.equals("true"))
    	{
    		if(DEBUG)
    			Log.e(NotepadStrings.LOG_TAG, "Response <saved> != true:\n" + result + "\nRequest was: \n" + requestString );
    		else
    			Log.e(NotepadStrings.LOG_TAG, "Response <saved> != true:\n" + result );

    		errorWhileSyncingTheseTitles.append(serverSideNoteInfo.server_title.replace('\n', ' ') + "\n");

    		return false;
    	}

		deleteNoteFromPhone(noteUri);

		return true;
	}



	/**
	 * Uploads the note to the server.
	 * @param localNoteInfo
	 * @param serverSideNoteInfo Can be null
	 * @param serverClockLag
	 *
	 * @return True if successful
	 *
	 */
	private boolean uploadNoteToServer(LocalNoteInfo localNoteInfo, ServerSideNoteInfo serverSideNoteInfo, int serverClockLag) throws IOException
	{
		final ContentResolver contentResolver = activity.getContentResolver();
		final Uri tableUri = activity.getIntent().getData();
		final Uri noteUri = ContentUris.withAppendedId(tableUri, localNoteInfo._id);

		// Get URI of notes database
		if(DEBUG)
		{
			Log.i(NotepadStrings.LOG_TAG, "ULOADING_NOTE: (" + serverSideNoteInfo.server_helipad_id + ") " + serverSideNoteInfo.server_title);
		}

		setText("Uploading:\n", localNoteInfo.local_title.replace('\n', ' '));


	   	//upload
    	final String requestString = "" +
    	"<request>" +
    		getAuthenticationSection() +
    		getDocumentSection(
    				localNoteInfo.local_title,
    				localNoteInfo.local_contents.equals(serverSideNoteInfo.server_contents) ? null : localNoteInfo.local_contents, //if contents haven't changed, don't upload them
    				localNoteInfo.local_tags.equals(serverSideNoteInfo.server_tags) ? null: localNoteInfo.local_tags) + //if tags haven't changed, don't upload them
    	"</request>";


    	String result = "";
    	try {
        	result = makeAPICall("http://pad.helicoid.net/document/"+serverSideNoteInfo.server_helipad_id+"/update", requestString, null);
    	} catch(IOException e) {
    		if(DEBUG)
    			Log.e(NotepadStrings.LOG_TAG, "Couldn't upload note: " + requestString, e);
    		else
    			Log.e(NotepadStrings.LOG_TAG, "Couldn't upload note: " + localNoteInfo.local_title, e);

    		errorWhileSyncingTheseTitles.append(localNoteInfo.local_title.replace('\n', ' ') + "\n");

    		return false;
    	}

    	//parse response
    	String saved;
    	try  {
    		saved = parseValueFromResponse(result, "saved");
    	} catch(ParseException e) {
    		if(DEBUG)
    			Log.e(NotepadStrings.LOG_TAG, "Couldn't parse response:\n" + result + "\nRequest was: \n" + requestString, e);
    		else
    			Log.e(NotepadStrings.LOG_TAG, "Couldn't parse response:\n" + result, e);

			//TODO LOCALYTICS TAG
    		errorWhileSyncingTheseTitles.append(localNoteInfo.local_title.replace('\n', ' ') + "\n");

    		return false;
    	}

    	if(!saved.equals("true"))
    	{
    		if(DEBUG)
    			Log.e(NotepadStrings.LOG_TAG, "Response <saved> != true:\n" + result + "\nRequest was: \n" + requestString );
    		else
    			Log.e(NotepadStrings.LOG_TAG, "Response <saved> != true:\n" + result );

    		errorWhileSyncingTheseTitles.append(localNoteInfo.local_title.replace('\n', ' ') + "\n");

    		return false;
    	}

    	//update sync time
    	int now = (int) (System.currentTimeMillis() / 1000);
        ContentValues values = new ContentValues();

        //if server clock is ahead of local clock, set the SYNCED_ON time ahead
        //(DOWNSIDE: won't upload until this difference elapses, even if changes made on phone version)
        int serverClock = now - serverClockLag;
        values.put(DBConstants.SYNCED_ON, Math.max(now, serverClock + 3) );  //add 3 seconds to be sure
        values.put(DBConstants.DONT_SYNC_THIS_NOTE, 0);
		values.put(DBConstants.DELETED_FROM_PHONE, 0);

        int updated = contentResolver.update(noteUri, values, null, null);
        if(updated <= 0) {
			//TODO LOCALYTICS TAG
        	fatalError.append("Couldn't update database" + "\n");
    		throw new IOException("Couldn't update note: " + noteUri);
    	}

        return true;
	}

	/**
	 * "Downloads" a note to the phone. Takes a note from the server and updates its record on the phone's data base.
	 * @param serverSideNoteInfo
	 * @throws IOException
	 */
	private void downloadNoteToPhone(LocalNoteInfo localNoteInfo, ServerSideNoteInfo serverSideNoteInfo) throws IOException
	{
		// Get URI of notes database
		final ContentResolver contentResolver = activity.getContentResolver();
		final Uri tableUri = activity.getIntent().getData();
		final Uri noteUri = ContentUris.withAppendedId(tableUri, localNoteInfo._id);

		int now = (int) (System.currentTimeMillis() / 1000);
		ContentValues values = new ContentValues();
		values.put(DBConstants.REMOTE_ID, serverSideNoteInfo.server_helipad_id);
		values.put(DBConstants.TITLE, serverSideNoteInfo.server_title);
		values.put(DBConstants.CONTENTS, serverSideNoteInfo.server_contents);
		values.put(DBConstants.TAGS, serverSideNoteInfo.server_tags);
		values.put(DBConstants.CREATED_ON, serverSideNoteInfo.server_created_on);
		values.put(DBConstants.MODIFIED_ON, serverSideNoteInfo.server_modified_on);
		values.put(DBConstants.SYNCED_ON, Math.max(now, serverSideNoteInfo.server_modified_on));
		values.put(DBConstants.DONT_SYNC_THIS_NOTE, 0);
		values.put(DBConstants.DELETED_FROM_PHONE, 0);

		int updated = contentResolver.update(noteUri, values, null, null);
		if(updated <= 0) {
			//TODO LOCALYTICS TAG
			fatalError.append("Couldn't update database" + "\n");
			throw new IOException("Couldn't update note: " + noteUri);
		}
	}

	/**
	 * Makes a call to the helipad API. Returns response.
	 * @param urlString
	 * @param postContent (optional) The POST data, or null.
	 * @param serverClockLag (optional) new long[1] or null. If not null, the array element will be set to (phone time - server time) in seconds, or Integer.MAX_VALUE if server clock unknown
	 * @return
	 * @throws IOException
	 */
	private String makeAPICall(String urlString, String postContent, int[] serverClockLag) throws IOException
    {
        URL url = new URL(urlString);
        URLConnection conn = url.openConnection();

        if (!(conn instanceof HttpURLConnection))
            throw new IOException("Not an HTTP connection");


        //Log.w("BEN", "MAKING API CALL: " + urlString + (postContent == null ? "  HTTP GET" : "\n" + postContent) );
        HttpURLConnection httpConn = (HttpURLConnection) conn;
        httpConn.setAllowUserInteraction(false);
        httpConn.setInstanceFollowRedirects(true);
        httpConn.setRequestMethod( postContent == null ? "GET" : "POST" );
        httpConn.addRequestProperty("Accept", "application/xml");
        httpConn.addRequestProperty("Content-Type", "application/xml");
        httpConn.setDoInput(true);
        if(postContent != null)
        {
        	httpConn.setUseCaches(false);
        	httpConn.setDoOutput(true);
        	httpConn.addRequestProperty("Content-Length", Integer.toString(postContent.length()));
            DataOutputStream out = new DataOutputStream( httpConn.getOutputStream() );
            out.writeBytes( postContent );
            out.flush();
            out.close();
        }


        //if serverClockLag arg provided, figure out
        if(serverClockLag != null ) {

        	long serverClock = httpConn.getHeaderFieldDate("date", -1);
        	if(serverClock != -1) {
        		serverClockLag[0] = (int) ( (System.currentTimeMillis() - serverClock)/1000 );
        	} else {
        		serverClockLag[0] = Integer.MAX_VALUE;
        	}

        	if(DEBUG) {
        		Log.i(NotepadStrings.LOG_TAG, "Local time - server time: " + serverClockLag[0] + " seconds, Server clock: " + httpConn.getHeaderField("date"));
        	}
        }

        //Log.i(NotePad.LOG_TAG, "Response code: " + response);
        int responseCode = httpConn.getResponseCode();
        if (responseCode == HttpURLConnection.HTTP_OK) {

            BufferedReader reader = new BufferedReader( new InputStreamReader( httpConn.getInputStream(), "UTF-8" ), 8000 );
       		StringBuffer responeAccumulator = new StringBuffer(16000);

       		//read the response. If the response starts with a <documents..> tag, then
       		String currentLine = null;
			while( (currentLine = reader.readLine()) != null)
       		{
				responeAccumulator.append(currentLine).append("\n");
       		}

       		return responeAccumulator.toString();
        }
        else
        {
        	throw new IOException("Bad response code: " + responseCode);
        }
    }


	/**
	 * Tells the SyncThread to exit.
	 */
	public void requestStop()
	{
		//Log.i(NotepadStrings.LOG_TAG, "SyncThread: STOP REQUESTED");
		stop = true;
	}

	public boolean isStopRequested()
	{
		return stop;
	}


	/**
	 * Returns the contents of the given tag
	 * @param response
	 * @param tag
	 * @return
	 */
	private static final String parseValueFromResponse(String response, String tag) throws ParseException
	{
		//check for empty tag (eg. <tags type="array"/> )
		final int single_tag_index1 = response.indexOf("<"+tag);
		final int single_tag_index2 = response.indexOf("/>", single_tag_index1 < 0 ? 0 : single_tag_index1);
		final int single_tag_index3 = response.indexOf(">", single_tag_index1 < 0 ? 0 : single_tag_index1);

		if(single_tag_index1 >= 0 && single_tag_index2 >= 0 && single_tag_index3 > single_tag_index2) {
			return "";
		}

		final int open_tag_index1 = response.indexOf("<"+tag);
		final int open_tag_index2 = response.indexOf(">", open_tag_index1 < 0 ? 0 : open_tag_index1 ); //skip attributes
		if(open_tag_index1 < 0 || open_tag_index2 < 0) {
			throw new ParseException("<"+tag+"> not found (" + open_tag_index1 + "," + open_tag_index2 + "): " + response, 0);
		}

		int close_tag_index = response.indexOf("</"+tag+">");
		if(close_tag_index < 0) {
			throw new ParseException("</"+tag+"> not found in: " + response, 0);
		}

		return response.substring(open_tag_index2 + 1, close_tag_index);
	}


	/**
	 * Used to construct the authentication part of an API call.
	 * @return
	 */
	private final String getAuthenticationSection()
	{
		return "<authentication><email>"+username+"</email><password>"+password+"</password></authentication>";
	}



	/**
	 * Used to construct the document description part of an API call.
	 * @return
	 */
	private final String getDocumentSection(String title, String contents, String tagsSeparatedWithSpaces)
	{


		return "<document><title>"+ encodeXMLSpecialChars(title) +"</title>" +
					(contents == null ? "" : "<source>"+ encodeXMLSpecialChars(contents) +"</source>") +
					(tagsSeparatedWithSpaces == null ? "" :"<tags>"+ encodeXMLSpecialChars(tagsSeparatedWithSpaces)+"</tags>") +
				"</document>";
	}

	/**
	 * Handles special chars in the XML request, response.
	 * @param encodedXML
	 * @return
	 */
	private final static String decodeXMLEscapeChars(String encodedXML)
	{
		//handle special html chars
		String result = encodedXML;
		result = result.replace("&lt;", "<").replace("&gt;", ">");
		result = result.replace("&amp;", "&");

		//handle any unicode chars (eg. &#12512;)
		int andIndex = 0;
		while((andIndex = result.indexOf("&#", andIndex)) >= 0) {
			andIndex++;
			int semiColonIndex = result.indexOf(';', andIndex);
			if(semiColonIndex < 0) {
				continue;
			}

			String unicodeNumber =  result.substring(andIndex+1, semiColonIndex);
			try
			{
				char unicodeChar = (char) Integer.parseInt( unicodeNumber );
				result = result.substring(0, andIndex - 1) + unicodeChar + result.substring(semiColonIndex + 1);
			}
			catch(Exception e)
			{
				Log.e("Notepad", "Failed to decode unicode char: " + unicodeNumber, e);
				continue;
			}
		}

		return result; //.replace("&quot;", "\"").replace("&apos;", "'");
	}

	/**
	 * Handles special chars in the XML request, response.
	 * @param encodedXML
	 * @return
	 */
	private final static String encodeXMLSpecialChars(String raw)
	{
		raw = raw.replace("&", "&amp;");
		raw = raw.replace("<", "&lt;").replace(">", "&gt;");
		//.replace("\"", "&quot;").replace("'", "&apos;").replace("&", "&amp;");

		//handle any non-ASCII unicode chars
		StringBuffer result = new StringBuffer(raw.length());
		for(int i = 0; i < raw.length(); i++)
		{
			int unicodeNumber = raw.codePointAt(i);
			if(unicodeNumber < 128)
			{
				result.append((char) unicodeNumber);

			}
			else
			{
				//convert all non-ascii characters to their html unicode-escape equivalents
				result.append("&#").append(Integer.toString(unicodeNumber)).append(";");
			}
		}


		return result.toString();
	}



	/**
	 * Changes the message in the Sync dialog.
	 * @param mediumText
	 * @param smallText
	 */
	private void setText(final String mediumText, final String smallText)
	{
		mHandler.post(new Runnable() {

			public void run() {

				activity.setText(mediumText, smallText);
			}
		});
	}

	/**
     * Shows the button panel and sets the buttons.
     */
    protected void showButtonPanel(final String[] buttonText, final OnClickListener buttonListener)
    {
    	mHandler.post(new Runnable() {

			public void run()
			{
				activity.showButtonPanel(buttonText, buttonListener);
			}
		});
    }


    /**
     * Hides the button panel
     */
    public void hideButtonPanel()
    {
    	mHandler.post(new Runnable() {

			public void run() {

				activity.hideButtonPanel();
			}
		});
    }

	/**
	 * Tags an event for localytics stats.
	 *
	 * @param tag The event tag name (see {@link LocalyticsConstants})
	 */
	private void localyticsTagEvent(String tag)
	{
		if(LocalyticsConstants.ENABLE_SUB_ACTIVITIY_TAGGING)
		{
			LocalyticsSession localyticsSession = new LocalyticsSession(
		            activity,  // Context used to access device resources
		            activity.getLocalyticsAppKey()); // Key generated on the webservice
			localyticsSession.tagEvent(tag);
		}

	}




	/**
	 * Queries user and doesn't return until the user chooses a result.
	 * @return the button # chosen (starting from 0)
	 */
	private int askUser( String[] buttonText ) throws IOException
	{
		ButtonListener listener = new ButtonListener(buttonText);
		showButtonPanel(buttonText, listener);

		while(listener.getSelection() == null)
		{
			//wait for user to make a choice
			try {
				Thread.sleep(30);

				if(stop)
				{
					break;
				}
			} catch(Exception e) {

			}
		}

		hideButtonPanel();

		if(stop)
		{
			fatalError.append("canceled");
			throw new IOException("Stop requested");
		}

		return listener.getSelection();
	}


	/**
	 * Parses timestamp of the form 2009-05-24T17:56:46+01:00
	 * @param timestamp
	 * @return
	 * @throws ParseException
	 */
	private static final int parseTimestamp(String timestamp) throws ParseException
	{
		return (int) (dateFormat.parse(timestamp).getTime() / 1000);
	}




	/**
	 * Different possible scenarios for resolving conflicting edits on client, server.
	 */
	private enum ConflictResolutionOptions
	{
		DOWNLOAD_TO_PHONE,
		UPLOAD_TO_SERVER,
		CREATE_ON_SERVER,
		DELETE_FROM_PHONE,
		DELETE_FROM_SERVER,
		SKIP,
		;

		@Override
		public String toString() {
			switch(this)
			{
			case DELETE_FROM_PHONE:
				return "Delete\nfrom phone";
			case DELETE_FROM_SERVER:
				return "Delete\nfrom server";
			case SKIP:
				return "Skip";
			case DOWNLOAD_TO_PHONE:
				return "Download\nto phone";
			case UPLOAD_TO_SERVER:
				return "Upload\nto server";
			case CREATE_ON_SERVER:
				return "Upload\nto server";
			default:
				throw new IllegalStateException("Unexpected enum: " + this);
			}
		}

	}



	/**
	 * Button listener.
	 */
	protected class ButtonListener implements OnClickListener
	{

		String[] buttonText;
		Integer selection = null;

		public ButtonListener(String[] buttonText)
		{
			this.buttonText = buttonText;
		}

		@Override
		public void onClick(View v)
		{
			for(int i = 0; i < buttonText.length; i++)
			{
				if( ((Button) v).getText().equals( buttonText[i] ) )
				{
					selection = i;
					break;
				}
			}
		}

		/**
		 * Returns null until a selection has been made, at which point it returns the button # that was pressed.
		 * @return
		 */
		public Integer getSelection()
		{
			return selection;
		}
	};

	/**
	 * Info about a note received from the Helipad server
	 */
	protected class ServerSideNoteInfo
	{
		public String server_helipad_id;
		public String server_title;
		public String server_contents;
		public String server_tags;
		public int server_created_on;
		public int server_modified_on;

	}

	/**
	 * Info about a note stored on the phone.
	 */
	protected class LocalNoteInfo
	{
		public int _id;
		public String local_title;
		public String local_contents;
		public String local_tags;
		public boolean is_deleted_from_phone;
		public int dont_sync_this_note;
		public int local_created_on;
		public int local_modified_on;
		public int synced_on;

	}

	/**
	 * Sets a handler which will execute when the thread is about1 to finish.
	 * It will be one of the last things the thread executes.
	 *
	 * @param runnable
	 */
	public void setThreadFinishedHandler(Runnable runnable)
	{
		this.threadFinishedHandler  = runnable;
	}
}
